package com.lazybones.base.engineer.utils.google;

import org.apache.commons.codec.binary.Base32;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;

public class GoogleAuthenticator {
    private final static Logger log = LoggerFactory.getLogger(GoogleAuthenticator.class);
    /**
     * 时间前后偏移量
     * 用于防止客户端时间不精确导致生成的TOTP与服务器端的TOTP一直不一致
     * 如果为0,当前时间为 10:10:15
     * 则表明在 10:10:00-10:10:30 之间生成的TOTP 能校验通过
     * 如果为1,则表明在
     * 10:09:30-10:10:00
     * 10:10:00-10:10:30
     * 10:10:30-10:11:00 之间生成的TOTP 能校验通过
     * 以此类推
     */
    private static int WINDOW_SIZE = 0;
    /**
     * 加密方式，HmacSHA1、HmacSHA256、HmacSHA512
     */
    private static final String CRYPTO = "HmacSHA1";
    /**
     * 生成密钥
     *
     * @return 密钥
     */
    public static String createSecret() {
        SecureRandom random = new SecureRandom();
        byte[] bytes = new byte[20];
        random.nextBytes(bytes);
        Base32 base32 = new Base32();
        String secret = base32.encodeToString(bytes);
        log.info("create secret key success {}",secret);
        return secret.toUpperCase();
    }
    /**
     * 生成二维码内容
     *
     * @param secret  密钥
     * @param account 账户名
     * @param issuer  网站地址（可不写）
     * @return 二维码内容
     */
    public static String createQrCodeText(String secret, String account, String issuer) {
        String normalizedBase32Key = secret.replace(" ", "").toUpperCase();
      /*  // 开始拼接
        StringBuilder str = new StringBuilder("otpauth://totp/");
        // 账号名
        str.append(account).append("?");
        // 密钥
        str.append("secret=").append(normalizedBase32Key);
        // 签发者
        if (StringUtils.isNotBlank(issuer)) {
            str.append("&issuer=").append(issuer);
        }
        log.info("create link path is {}",str);*/
        return "otpauth://totp/"
                + URLEncoder.encode((!StringUtils.isEmpty(issuer) ? (issuer + ":") : "") + account, StandardCharsets.UTF_8).replace("+", "%20")
                + "?secret=" + URLEncoder.encode(normalizedBase32Key, StandardCharsets.UTF_8).replace("+", "%20")
                + (!StringUtils.isEmpty(issuer) ? ("&issuer=" + URLEncoder.encode(issuer, StandardCharsets.UTF_8).replace("+", "%20")) : "");
    }
    /**
     * 获取验证码
     *
     * @param secret 密钥
     * @return 验证码
     */
    public static String getCode(String secret) {
        String normalizedBase32Key = secret.replace(" ", "").toUpperCase();
        Base32 base32 = new Base32();
        byte[] bytes = base32.decode(normalizedBase32Key);
        String hexKey = Hex.encodeHexString(bytes);
        long time = (System.currentTimeMillis() / 1000) / 30;
        String hexTime = Long.toHexString(time);
        return VerifyNumUtils.generateVerify(hexKey, hexTime, "6", CRYPTO);
    }
    /**
     * 检验 code 是否正确
     *
     * @param secret 密钥
     * @param code   code
     * @param time   时间戳
     * @return 校验结果
     */
    public static boolean checkCode(String secret, long code, long time) {
        Base32 codec = new Base32();
        byte[] decodedKey = codec.decode(secret);
        long t = (time / 1000L) / 30L;
        long hash;
        for (int i = -WINDOW_SIZE; i <= WINDOW_SIZE; ++i) {
            try {
                hash = verifyCode(decodedKey, t + i);
            } catch (Exception e) {
                throw new RuntimeException(e.getMessage());
            }
            if (hash == code) {
                return true;
            }
        }
        return false;
    }

    /**
     * 根据时间偏移量计算
     *
     * @param key
     * @param t
     * @return
     * @throws NoSuchAlgorithmException
     * @throws InvalidKeyException
     */
    private static long verifyCode(byte[] key, long t) throws NoSuchAlgorithmException, InvalidKeyException {
        byte[] data = new byte[8];
        long value = t;
        for (int i = 8; i-- > 0; value >>>= 8) {
            data[i] = (byte) value;
        }
        SecretKeySpec signKey = new SecretKeySpec(key, CRYPTO);
        Mac mac = Mac.getInstance(CRYPTO);
        mac.init(signKey);
        byte[] hash = mac.doFinal(data);
        int offset = hash[20 - 1] & 0xF;
        long truncatedHash = 0;
        for (int i = 0; i < 4; ++i) {
            truncatedHash <<= 8;
            truncatedHash |= (hash[offset + i] & 0xFF);
        }
        truncatedHash &= 0x7FFFFFFF;
        truncatedHash %= 1000000;
        return truncatedHash;
    }

}
